package gohcl

import (
    "fmt"
    "reflect"

    "github.com/zclconf/go-cty/cty"

    "Havoc/pkg/profile/yaotl"
    "github.com/zclconf/go-cty/cty/convert"
    "github.com/zclconf/go-cty/cty/gocty"
)

// DecodeBody extracts the configuration within the given body into the given
// value. This value must be a non-nil pointer to either a struct or
// a map, where in the former case the configuration will be decoded using
// struct tags and in the latter case only attributes are allowed and their
// values are decoded into the map.
//
// The given EvalContext is used to resolve any variables or functions in
// expressions encountered while decoding. This may be nil to require only
// constant values, for simple applications that do not support variables or
// functions.
//
// The returned diagnostics should be inspected with its HasErrors method to
// determine if the populated value is valid and complete. If error diagnostics
// are returned then the given value may have been partially-populated but
// may still be accessed by a careful caller for static analysis and editor
// integration use-cases.
func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
    rv := reflect.ValueOf(val)
    if rv.Kind() != reflect.Ptr {
        panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String()))
    }

    return decodeBodyToValue(body, ctx, rv.Elem())
}

func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
    et := val.Type()
    switch et.Kind() {
    case reflect.Struct:
        return decodeBodyToStruct(body, ctx, val)
    case reflect.Map:
        return decodeBodyToMap(body, ctx, val)
    default:
        panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String()))
    }
}

func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
    schema, partial := ImpliedBodySchema(val.Interface())

    var content *hcl.BodyContent
    var leftovers hcl.Body
    var diags hcl.Diagnostics
    if partial {
        content, leftovers, diags = body.PartialContent(schema)
    } else {
        content, diags = body.Content(schema)
    }
    if content == nil {
        return diags
    }

    tags := getFieldTags(val.Type())

    if tags.Body != nil {
        fieldIdx := *tags.Body
        field := val.Type().Field(fieldIdx)
        fieldV := val.Field(fieldIdx)
        switch {
        case bodyType.AssignableTo(field.Type):
            fieldV.Set(reflect.ValueOf(body))

        default:
            diags = append(diags, decodeBodyToValue(body, ctx, fieldV)...)
        }
    }

    if tags.Remain != nil {
        fieldIdx := *tags.Remain
        field := val.Type().Field(fieldIdx)
        fieldV := val.Field(fieldIdx)
        switch {
        case bodyType.AssignableTo(field.Type):
            fieldV.Set(reflect.ValueOf(leftovers))
        case attrsType.AssignableTo(field.Type):
            attrs, attrsDiags := leftovers.JustAttributes()
            if len(attrsDiags) > 0 {
                diags = append(diags, attrsDiags...)
            }
            fieldV.Set(reflect.ValueOf(attrs))
        default:
            diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...)
        }
    }

    for name, fieldIdx := range tags.Attributes {
        attr := content.Attributes[name]
        field := val.Type().Field(fieldIdx)
        fieldV := val.Field(fieldIdx)

        if attr == nil {
            if !exprType.AssignableTo(field.Type) {
                continue
            }

            // As a special case, if the target is of type hcl.Expression then
            // we'll assign an actual expression that evalues to a cty null,
            // so the caller can deal with it within the cty realm rather
            // than within the Go realm.
            synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
            fieldV.Set(reflect.ValueOf(synthExpr))
            continue
        }

        switch {
        case attrType.AssignableTo(field.Type):
            fieldV.Set(reflect.ValueOf(attr))
        case exprType.AssignableTo(field.Type):
            fieldV.Set(reflect.ValueOf(attr.Expr))
        default:
            diags = append(diags, DecodeExpression(
                attr.Expr, ctx, fieldV.Addr().Interface(),
            )...)
        }
    }

    blocksByType := content.Blocks.ByType()

    for typeName, fieldIdx := range tags.Blocks {
        blocks := blocksByType[typeName]
        field := val.Type().Field(fieldIdx)

        ty := field.Type
        isSlice := false
        isPtr := false
        if ty.Kind() == reflect.Slice {
            isSlice = true
            ty = ty.Elem()
        }
        if ty.Kind() == reflect.Ptr {
            isPtr = true
            ty = ty.Elem()
        }

        if len(blocks) > 1 && !isSlice {
            diags = append(diags, &hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  fmt.Sprintf("Duplicate %s block", typeName),
                Detail: fmt.Sprintf(
                    "Only one %s block is allowed. Another was defined at %s.",
                    typeName, blocks[0].DefRange.String(),
                ),
                Subject: &blocks[1].DefRange,
            })
            continue
        }

        if len(blocks) == 0 {
            if isSlice || isPtr {
                if val.Field(fieldIdx).IsNil() {
                    val.Field(fieldIdx).Set(reflect.Zero(field.Type))
                }
            } else {
                diags = append(diags, &hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  fmt.Sprintf("Missing %s block", typeName),
                    Detail:   fmt.Sprintf("A %s block is required.", typeName),
                    Subject:  body.MissingItemRange().Ptr(),
                })
            }
            continue
        }

        switch {

        case isSlice:
            elemType := ty
            if isPtr {
                elemType = reflect.PtrTo(ty)
            }
            sli := val.Field(fieldIdx)
            if sli.IsNil() {
                sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
            }

            for i, block := range blocks {
                if isPtr {
                    if i >= sli.Len() {
                        sli = reflect.Append(sli, reflect.New(ty))
                    }
                    v := sli.Index(i)
                    if v.IsNil() {
                        v = reflect.New(ty)
                    }
                    diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
                    sli.Index(i).Set(v)
                } else {
                    if i >= sli.Len() {
                        sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty)))
                    }
                    diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...)
                }
            }

            if sli.Len() > len(blocks) {
                sli.SetLen(len(blocks))
            }

            val.Field(fieldIdx).Set(sli)

        default:
            block := blocks[0]
            if isPtr {
                v := val.Field(fieldIdx)
                if v.IsNil() {
                    v = reflect.New(ty)
                }
                diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
                val.Field(fieldIdx).Set(v)
            } else {
                diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...)
            }

        }

    }

    return diags
}

func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
    attrs, diags := body.JustAttributes()
    if attrs == nil {
        return diags
    }

    mv := reflect.MakeMap(v.Type())

    for k, attr := range attrs {
        switch {
        case attrType.AssignableTo(v.Type().Elem()):
            mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr))
        case exprType.AssignableTo(v.Type().Elem()):
            mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr))
        default:
            ev := reflect.New(v.Type().Elem())
            diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...)
            mv.SetMapIndex(reflect.ValueOf(k), ev.Elem())
        }
    }

    v.Set(mv)

    return diags
}

func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
    var diags hcl.Diagnostics

    ty := v.Type()

    switch {
    case blockType.AssignableTo(ty):
        v.Elem().Set(reflect.ValueOf(block))
    case bodyType.AssignableTo(ty):
        v.Elem().Set(reflect.ValueOf(block.Body))
    case attrsType.AssignableTo(ty):
        attrs, attrsDiags := block.Body.JustAttributes()
        if len(attrsDiags) > 0 {
            diags = append(diags, attrsDiags...)
        }
        v.Elem().Set(reflect.ValueOf(attrs))
    default:
        diags = append(diags, decodeBodyToValue(block.Body, ctx, v)...)

        if len(block.Labels) > 0 {
            blockTags := getFieldTags(ty)
            for li, lv := range block.Labels {
                lfieldIdx := blockTags.Labels[li].FieldIndex
                v.Field(lfieldIdx).Set(reflect.ValueOf(lv))
            }
        }

    }

    return diags
}

// DecodeExpression extracts the value of the given expression into the given
// value. This value must be something that gocty is able to decode into,
// since the final decoding is delegated to that package.
//
// The given EvalContext is used to resolve any variables or functions in
// expressions encountered while decoding. This may be nil to require only
// constant values, for simple applications that do not support variables or
// functions.
//
// The returned diagnostics should be inspected with its HasErrors method to
// determine if the populated value is valid and complete. If error diagnostics
// are returned then the given value may have been partially-populated but
// may still be accessed by a careful caller for static analysis and editor
// integration use-cases.
func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
    srcVal, diags := expr.Value(ctx)

    convTy, err := gocty.ImpliedType(val)
    if err != nil {
        panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err))
    }

    srcVal, err = convert.Convert(srcVal, convTy)
    if err != nil {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Unsuitable value type",
            Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
            Subject:  expr.StartRange().Ptr(),
            Context:  expr.Range().Ptr(),
        })
        return diags
    }

    err = gocty.FromCtyValue(srcVal, val)
    if err != nil {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Unsuitable value type",
            Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
            Subject:  expr.StartRange().Ptr(),
            Context:  expr.Range().Ptr(),
        })
    }

    return diags
}
